package org.u2ml.stack.xml.xstream;

import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.collection.PersistentBag;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.collection.PersistentList;
import org.hibernate.collection.PersistentMap;
import org.hibernate.collection.PersistentSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Service;
import org.u2ml.Main;
import org.u2ml.stack.xml.XmlStreamer;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamConverter;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.mapper.LocalConversionMapper;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;

@Service("xmlStreamer")
public class XStreamXmlStreamer implements XmlStreamer {
	private final Logger logger = LoggerFactory.getLogger(XStreamXmlStreamer.class);
	
	private XStream xstream;

	private Collection<Class<?>> mappedClasses = new ArrayList<Class<?>>();

	private LocalConversionMapper localConversionMapper;
	
	@SuppressWarnings("deprecation")
	public XStreamXmlStreamer() {
		// xstream and hibernate do not play together very well by default, here are some xstream tricks 
		// to serialize and deserialize only what we want
		xstream = new XStream(new DomDriver("UTF-8"));
		xstream = new XStream(new PureJavaReflectionProvider(), new MapperWrapper(xstream.getMapper()) {
			@Override
			public boolean shouldSerializeMember(Class definedIn,
					String fieldName) {
				if (PersistentCollection.class.isAssignableFrom(definedIn)) {
					// do not serialize hibernate specific fields
					return false;
				}
				return super.shouldSerializeMember(definedIn, fieldName);
			}
			@Override
			public String serializedClass(Class type) {
				// we use this to avoid serializing the class of hibernate specific collections
				if (PersistentList.class == type || PersistentBag.class == type) {
					return super.serializedClass(defaultImplementationOf(List.class));
				}
				if (PersistentSet.class == type) {
					return super.serializedClass(defaultImplementationOf(Set.class));
				}
				if (PersistentMap.class == type) {
					return super.serializedClass(defaultImplementationOf(Map.class));
				}
				// default to default behaviour
				return super.serializedClass(type);
			}
		}, new DomDriver("UTF-8"));
		
		Mapper mapper = xstream.getMapper();
		xstream.registerConverter(new HibernateCollectionConverter(mapper));
		xstream.registerConverter(new HibernateMapConverter(mapper));
		// end of xstream/hibernate tricks
		
		xstream.registerConverter(new DateConverter(), XStream.PRIORITY_VERY_HIGH);
		xstream.addDefaultImplementation(ArrayList.class, Collection.class);
		xstream.alias("list", Collection.class);
		
		localConversionMapper = (LocalConversionMapper) xstream.getMapper().lookupMapperOfType(LocalConversionMapper.class);
		/* REFERENCE AVEC L'IDENTIFIANT DES OBJETS */
		xstream.setMode(XStream.ID_REFERENCES);

		// auto discovery of xstream enabled classes based on spring annotation discovery mechanism
		ClassPathScanningCandidateComponentProvider entitiesScanner = 
			new ClassPathScanningCandidateComponentProvider(false);
		
		entitiesScanner.addIncludeFilter(new AnnotationTypeFilter(XStreamAlias.class));
		logger.debug("scanning classpath for xstream enabled classes...");
		Set<BeanDefinition> candidates = entitiesScanner.findCandidateComponents(Main.class.getPackage().getName());
		logger.info("found {} xstream enabled classes", candidates.size());
		
		for (BeanDefinition candidate : candidates) {
			try {
				// register the class to xstream
				Class<?> clazz = Class.forName(candidate.getBeanClassName());
				xstream.processAnnotations(clazz);
				mappedClasses.add(clazz);

				// XStreamConverter is not processed on objects themselves by xstream, let's do the work ourself
				XStreamConverter converter = clazz.getAnnotation(XStreamConverter.class);
				if (converter != null) {
					xstream.registerConverter((Converter) converter.value().newInstance());
				}
			} catch (ClassNotFoundException e) {
				throw new IllegalStateException(e);
			} catch (InstantiationException e) {
				throw new IllegalStateException(e);
			} catch (IllegalAccessException e) {
				throw new IllegalStateException(e);
			}
		}
	}
	
	public void registerLocalConverter(Object converter, String name) {
		if (! (converter instanceof Converter)) {
			throw new IllegalArgumentException(
						"converter " + converter + " must be a xstream Converter");
		}
		
		Converter c = (Converter) converter;
		for (Class<?> clazz : mappedClasses) {
			registerLocalConverter(clazz, c, name);
		}
	}
	
	private void registerLocalConverter(Class<?> clazz, Converter converter, String name) {
		XStreamClassConverter clAnnotation = clazz.getAnnotation(XStreamClassConverter.class);
		if (clAnnotation != null && clAnnotation.value().equals(name)) {
			logger.debug("registering converter {} for {}", converter, clazz);
			xstream.registerConverter(converter);
		}
		for (Field field : clazz.getDeclaredFields()) {
			XStreamFieldConverter annotation = field.getAnnotation(XStreamFieldConverter.class);
			if (annotation != null && annotation.value().equals(name)) {
				logger.debug("registering local converter {} for {}", converter, field);
				localConversionMapper.registerLocalConverter(
						field.getDeclaringClass(), field.getName(), converter);
			}
		}
	}

	public boolean isStreamable(Class<?> type) {
		return type.isAnnotationPresent(XStreamAlias.class) 
			|| type.isAnnotationPresent(javax.persistence.Entity.class)
			|| Collection.class.isAssignableFrom(type)
			|| Map.class.isAssignableFrom(type);
	}
	
	public <T> T fromXML(InputStream stream, Class<T> clazz) {
//		String string = null;
//		try {
//			string = IOUtils.toString(stream);
//			System.out.println(string);
//		} catch (IOException e) {
//			e.printStackTrace();
//		}
		return (T) xstream.fromXML(stream);
	}
	
	public <T> T fromXML(String string, Class<T> clazz) {
		return (T) xstream.fromXML(string);
	}
	
	public String toXML(Object obj) {
		return xstream.toXML(obj);
	}

	public void toXML(Object obj, OutputStream stream) {
		xstream.toXML(obj, stream);
	}	
}